Skip to content

Exempt Wolverine's Envelope from blanket multi-tenant doc policies (supersedes #2566 / marten#4268)#2570

Merged
jeremydmiller merged 2 commits intomainfrom
fix/2566-envelope-single-tenant-pin
Apr 22, 2026
Merged

Exempt Wolverine's Envelope from blanket multi-tenant doc policies (supersedes #2566 / marten#4268)#2570
jeremydmiller merged 2 commits intomainfrom
fix/2566-envelope-single-tenant-pin

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Supersedes #2566. Same change set, opened against the JasperFx repo so it can be merged directly. Closes #2566 once this lands.

Original reproducer authored by @erdtsieck (commit preserved with their authorship). Many thanks for the targeted repro — that's what made the root cause obvious.

The bug

Wolverine's internal Envelope outbox document silently inherited blanket Marten document policies (AllDocumentsAreMultiTenanted / AllDocumentsAreMultiTenantedWithPartitioning). When two stores share a database schema and one applies such a policy:

  • Store A (with the blanket policy) creates mt_doc_envelope as conjoined + hash-partitioned by tenant_id.
  • Store B (without it, or with a different one) describes Envelope as single-tenant. Its first SaveChangesAsync triggers a Marten schema diff that emits an impossible migration:
alter table … drop constraint pkey_mt_doc_envelope_id_tenant_id CASCADE;
alter table … add CONSTRAINT pkey_mt_doc_envelope_id PRIMARY KEY (id);  -- 0A000
alter table … drop column tenant_id;

Postgres rejects the ADD PRIMARY KEY (id) step on a still-partitioned table with 0A000: unique constraint on partitioned table must include all partitioning columns — exactly the error from marten#4268. Async projections hide the conflict; flipping to inline + EnableSideEffectsOnInlineProjections surfaces it on the first append.

The fix

In MartenOverrides.Configure:

options.Schema.For<Envelope>()
    .SingleTenanted()
    .DoNotPartition();

The _alterations registered by Schema.For<T>() run on the DocumentMappingBuilder<T> after applyPolicies and applyPostPolicies during DocumentMapping construction, so blanket ForAllDocuments policies cannot override them. Marten itself uses the same pattern internally to exempt DeadLetterEvent.

We considered an IDocumentPolicy approach (lazier — wouldn't pre-register Envelope at all). It loses ordering: Policies.OnDocuments(...) always Insert(0)s into _policies, so a later-registered blanket policy overwrites the per-type fix. Schema-builder alterations are the only public Marten hook that consistently wins. No Marten changes needed for this PR.

Verification

Bug_4268 reproducer 1/1 pass (was failing pre-fix with the exact marten#4268 error)
MartenTests subset (Bugs + MartenOutbox + publish_messages + AncillaryStores) 52/52 pass
Wolverine CoreTests 1346/1346 pass

🤖 Generated with Claude Code

erdtsieck and others added 2 commits April 22, 2026 18:26
…ten#4268)

Wolverine's Envelope outbox document was silently picking up blanket
document policies applied to a store — most painfully
options.Policies.AllDocumentsAreMultiTenantedWithPartitioning. That
turned mt_doc_envelope into a hash-partitioned, conjoined-tenant table.
When a second store sharing the same schema described Envelope as
single-tenant (its normal default, since Wolverine never asks for
multi-tenancy on its own operational table), Marten's schema diff
emitted an impossible delta:

    drop constraint pkey_mt_doc_envelope_id_tenant_id CASCADE;
    add CONSTRAINT pkey_mt_doc_envelope_id PRIMARY KEY (id);  -- 0A000
    drop column tenant_id;

Postgres rejects the ADD PRIMARY KEY on a still-partitioned table with
"unique constraint on partitioned table must include all partitioning
columns". Async projections hid the conflict; inline projections surface
it on the first SaveChanges.

MartenOverrides.Configure now pins Envelope as single-tenant /
unpartitioned via Schema.For<Envelope>().SingleTenanted().DoNotPartition().
Those alterations land on the DocumentMappingBuilder<T> _alterations
list, which fires AFTER applyPolicies + applyPostPolicies during
DocumentMapping construction, so blanket ForAllDocuments policies can't
override them.

Updated the reproducer from #2566 to assert the new guarantee: Envelope
is NOT tenant-partitioned after the async-projection store runs, and the
subsequent inline-side-effects store does not throw
MartenSchemaException on its first SaveChanges.

Verification:
- Bug_4268 reproducer: 1/1 pass (was failing pre-fix)
- MartenTests subset (Bugs + MartenOutbox + publish_messages + AncillaryStores): 52/52 pass
- Wolverine CoreTests: 1346/1346 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants